<template>
	<div class="tiptap-editor">
		<div v-if="editor" class="toolbar">
			<el-button-group>
				<!-- 标题选择 -->
				<el-select v-model="headingLevel" placeholder="标题" size="small" @change="toggleHeading">
					<el-option :value="0" label="正文" />
					<el-option :value="1" label="H1" />
					<el-option :value="2" label="H2" />
					<el-option :value="3" label="H3" />
					<el-option :value="4" label="H4" />
				</el-select>

				<!-- 字体家族 -->
				<el-select v-model="fontFamily" placeholder="字体" size="small" @change="editor.chain().focus().setFontFamily($event).run()">
					<el-option value="微软雅黑" label="微软雅黑" />
					<el-option value="宋体" label="宋体" />
					<el-option value="Arial" label="Arial" />
					<el-option value="Times New Roman" label="Times New Roman" />
					<el-option value="Courier New" label="Courier New" />
				</el-select>

				<el-select
					v-model="lineHeight"
					placeholder="行间距"
					size="small"
					style="width: 100px"
					@change="val => (val ? editor.commands.setLineHeight(val) : editor.commands.unsetLineHeight())"
				>
					<el-option :value="null" label="默认" />
					<el-option value="1.0" label="1.0" />
					<el-option value="1.5" label="1.5" />
					<el-option value="2.0" label="2.0" />
					<el-option value="2.5" label="2.5" />
					<el-option value="3.0" label="3.0" />
				</el-select>

				<!-- 样式格式组 -->
				<el-button-group>
					<el-button size="small" :class="{ 'is-active': editor.isActive('bold') }" @click="editor.chain().focus().toggleBold().run()">
						<strong>B</strong>
					</el-button>

					<el-button size="small" :class="{ 'is-active': editor.isActive('italic') }" @click="editor.chain().focus().toggleItalic().run()">
						<em>I</em>
					</el-button>

					<el-button size="small" :class="{ 'is-active': editor.isActive('underline') }" @click="editor.chain().focus().toggleUnderline().run()">
						<u>U</u>
					</el-button>

					<el-button size="small" :class="{ 'is-active': editor.isActive('strike') }" @click="editor.chain().focus().toggleStrike().run()">
						<s>S</s>
					</el-button>
				</el-button-group>

				<!-- 对齐方式 -->
				<el-button-group>
					<el-button
						size="small"
						:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
						@click="editor.chain().focus().setTextAlign('left').run()"
					>
						左
					</el-button>
					<el-button
						size="small"
						:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
						@click="editor.chain().focus().setTextAlign('center').run()"
					>
						中
					</el-button>
					<el-button
						size="small"
						:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
						@click="editor.chain().focus().setTextAlign('right').run()"
					>
						右
					</el-button>
					<el-button
						size="small"
						:class="{ 'is-active': editor.isActive({ textAlign: 'justify' }) }"
						@click="editor.chain().focus().setTextAlign('justify').run()"
					>
						两端
					</el-button>
				</el-button-group>

				<!-- 颜色选择 -->
				<el-button-group>
					<el-color-picker
						v-model="textColor"
						size="small"
						show-alpha
						:predefine="predefineColors"
						@change="editor.chain().focus().setColor($event).run()"
					/>

					<el-color-picker
						v-model="highlightColor"
						size="small"
						show-alpha
						:predefine="predefineColors"
						@change="editor.chain().focus().setHighlight({ color: $event }).run()"
					/>
				</el-button-group>

				<!-- 其他功能 -->
				<el-button-group>
					<el-button size="small" :class="{ 'is-active': editor.isActive('blockquote') }" @click="editor.chain().focus().toggleBlockquote().run()">
						引用
					</el-button>

					<el-button size="small" :class="{ 'is-active': editor.isActive('codeBlock') }" @click="editor.chain().focus().toggleCodeBlock().run()">
						代码块
					</el-button>

					<el-button size="small" @click="editor.chain().focus().setHorizontalRule().run()"> 分割线 </el-button>

					<el-button size="small" @click="showLinkDialog"> 链接 </el-button>

					<el-button size="small" @click="addImage">
						<el-icon><Picture /></el-icon>
					</el-button>
				</el-button-group>

				<el-button-group>
					<el-button size="small" @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"> 插入表格 </el-button>
					<el-button size="small" :disabled="!editor.can().addColumnAfter()" @click="editor.chain().focus().addColumnAfter().run()">
						添加列
					</el-button>
					<el-button size="small" :disabled="!editor.can().addRowAfter()" @click="editor.chain().focus().addRowAfter().run()"> 添加行 </el-button>
					<el-button size="small" :disabled="!editor.can().deleteColumn()" @click="editor.chain().focus().deleteColumn().run()"> 删除列 </el-button>
					<el-button size="small" :disabled="!editor.can().deleteRow()" @click="editor.chain().focus().deleteRow().run()"> 删除行 </el-button>
					<el-button size="small" :disabled="!editor.can().deleteTable()" @click="editor.chain().focus().deleteTable().run()"> 删除表格 </el-button>
				</el-button-group>

				<!-- 历史操作 -->
				<el-button-group>
					<el-button size="small" :disabled="!editor.can().undo()" @click="editor.chain().focus().undo().run()"> 撤销 </el-button>

					<el-button size="small" :disabled="!editor.can().redo()" @click="editor.chain().focus().redo().run()"> 重做 </el-button>
				</el-button-group>

				<!-- 清除格式 -->
				<el-button-group>
					<el-button size="small" @click="editor.chain().focus().clearNodes().unsetAllMarks().run()"> 清除格式 </el-button>
				</el-button-group>
			</el-button-group>
		</div>

		<!-- 链接对话框 -->
		<el-dialog v-model="linkDialogVisible" title="插入链接" width="30%">
			<el-input v-model="linkUrl" placeholder="请输入链接地址" />
			<template #footer>
				<el-button @click="linkDialogVisible = false">取消</el-button>
				<el-button type="primary" @click="insertLink">确认</el-button>
			</template>
		</el-dialog>

		<editor-content :editor="editor" class="editor-content" />
	</div>
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import TextAlign from '@tiptap/extension-text-align'
import FontFamily from '@tiptap/extension-font-family'
import Color from '@tiptap/extension-color'
import TextStyle from '@tiptap/extension-text-style'
import Heading from '@tiptap/extension-heading'
import Link from '@tiptap/extension-link'
import Blockquote from '@tiptap/extension-blockquote'
import CodeBlock from '@tiptap/extension-code-block'
import HorizontalRule from '@tiptap/extension-horizontal-rule'
import { Picture, VideoCamera } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import request from '@/utils/request'
import { ResizableImage } from './resizable-image'
import Underline from '@tiptap/extension-underline'
import { LineHeight } from './line-height'
import { Table } from '@tiptap/extension-table'
import { TableRow } from '@tiptap/extension-table-row'
import { TableHeader } from '@tiptap/extension-table-header'
import { TableCell } from '@tiptap/extension-table-cell'
import { Extension } from '@tiptap/core'
import { columnResizing } from 'prosemirror-tables'

const lineHeight = ref('')

const props = defineProps({
	modelValue: {
		type: String,
		default: ''
	}
})

const emit = defineEmits(['update:modelValue'])

const editor = useEditor({
	content: props.modelValue,
	extensions: [
		StarterKit.configure({
			marks: {
				bold: true,
				italic: true,
				underline: true,
				strike: true
			}
		}),
		Underline,
		ResizableImage.configure({
			inline: true,
			allowBase64: true,
			HTMLAttributes: {
				class: 'resizable-image'
			}
		}),
		TextStyle,
		LineHeight,
		Color,
		Heading.configure({ levels: [1, 2, 3, 4] }),
		TextAlign.configure({
			types: ['heading', 'paragraph'],
			alignments: ['left', 'center', 'right', 'justify']
		}),
		FontFamily,
		Link.configure({
			openOnClick: false,
			HTMLAttributes: {
				rel: 'noopener noreferrer'
			}
		}),
		Blockquote,
		CodeBlock,
		HorizontalRule,
		Table.configure({
			resizable: true,
			handleWidth: 5,
			cellMinWidth: 25
		}),
		TableRow,
		TableHeader,
		TableCell
	],
	onUpdate: () => {
		emit('update:modelValue', editor.value.getHTML())
	}
})

class ResizeHandleView {
	constructor() {
		this.handle = document.createElement('div')
		this.handle.className = 'resize-handle'
		this.handle.style.width = '5px'
		this.handle.style.backgroundColor = '#adadad'
		this.handle.style.position = 'absolute'
		this.handle.style.top = '0'
		this.handle.style.bottom = '0'
		this.handle.style.right = '-2px'
		this.handle.style.cursor = 'col-resize'
		this.handle.style.zIndex = '20'
	}

	update(view, cell) {
		const { node } = cell
		if (node.type.name !== 'table_header' && node.type.name !== 'table_cell') {
			return
		}

		const handle = this.handle
		const table = view.domAtPos(cell.pos).node
		const tableRect = table.getBoundingClientRect()
		const cellRect = cell.dom.getBoundingClientRect()

		handle.style.height = `${tableRect.height}px`
		handle.style.right = `${tableRect.right - cellRect.right - 2}px`
		return true
	}

	destroy() {
		this.handle.remove()
	}
}

const headingLevel = ref(0)
const fontFamily = ref('')
const textColor = ref('#000000')
const highlightColor = ref('#ffffff')
const linkDialogVisible = ref(false)
const linkUrl = ref('')
const predefineColors = ref([
	'#ff4500',
	'#ff8c00',
	'#ffd700',
	'#90ee90',
	'#00ced1',
	'#1e90ff',
	'#c71585',
	'rgba(255, 69, 0, 0.68)',
	'rgb(255, 120, 0)',
	'hsv(51, 100, 98)',
	'hsva(120, 40, 94, 0.5)',
	'hsl(181, 100%, 37%)',
	'hsla(209, 100%, 56%, 0.73)',
	'#c7158577'
])

const uploadFile = async (file, type) => {
	const formData = new FormData()
	formData.append('file', file)

	try {
		const { data } = await request.post('http://localhost:90/api/file/upload', formData, {
			headers: {
				'Content-Type': 'multipart/form-data'
			}
		})
		return data
	} catch (error) {
		ElMessage.error('上传失败')
		return null
	}
}

const addImage = async () => {
	if (!editor.value) {
		return
	}

	const input = document.createElement('input')
	input.type = 'file'
	input.accept = 'image/*'

	input.onchange = async () => {
		const file = input.files[0]
		if (!file) {
			return
		}

		if (file.size > 20 * 1024 * 1024) {
			ElMessage.error('图片大小不能超过20M')
			return
		}

		const result = await uploadFile(file, 'image')
		if (result) {
			editor.value
				.chain()
				.focus()
				.setImage({
					src: result,
					width: '400px',
					height: 'auto'
				})
				.run()
		}
	}

	input.click()
}

const addVideo = async () => {
	const input = document.createElement('input')
	input.type = 'file'
	input.accept = 'video/*'

	input.onchange = async () => {
		const file = input.files[0]
		if (!file) {
			return
		}

		if (file.size > 100 * 1024 * 1024) {
			ElMessage.error('视频大小不能超过100M')
			return
		}

		const result = await uploadFile(file, 'video')
		if (result) {
			editor.value.chain().focus().setVideo({ src: result }).run()
		}
	}

	input.click()
}

const updateActiveStates = () => {
	headingLevel.value = editor.value.getAttributes('heading').level || 0
	fontFamily.value = editor.value.getAttributes('textStyle')?.fontFamily || ''
	textColor.value = editor.value.getAttributes('textStyle')?.color || '#000000'
	highlightColor.value = editor.value.getAttributes('highlight')?.color || '#ffffff'
	lineHeight.value = editor.value.getAttributes('textStyle')?.lineHeight || null
}

watch(
	() => editor.value?.getAttributes('heading')?.level,
	level => {
		headingLevel.value = level || 0
	}
)

watch(
	() => editor.value?.getAttributes('textStyle')?.fontFamily,
	font => {
		fontFamily.value = font || ''
	}
)

watch(
	() => editor.value?.getAttributes('paragraph')?.lineHeight,
	newHeight => {
		lineHeight.value = newHeight || null
	}
)

watch(
	() => editor.value?.getAttributes('highlight').color,
	color => {
		highlightColor.value = color || '#ffffff'
	}
)

watch(
	() => editor.value?.getAttributes('fontFamily'),
	attrs => {
		fontFamily.value = attrs.fontFamily || ''
	}
)

const toggleHeading = () => {
	if (headingLevel.value === 0) {
		editor.value.chain().focus().setParagraph().run()
	} else {
		editor.value.chain().focus().toggleHeading({ level: headingLevel.value }).run()
	}
}

const showLinkDialog = () => {
	linkUrl.value = editor.value.getAttributes('link').href || ''
	linkDialogVisible.value = true
}

const insertLink = () => {
	if (linkUrl.value && editor.value) {
		editor.value.chain().focus().toggleLink({ href: linkUrl.value }).run()
	}
	linkDialogVisible.value = false
}

watch(
	() => props.modelValue,
	value => {
		const isSame = editor.value.getHTML() === value
		if (!isSame) {
			editor.value.commands.setContent(value)
		}
	}
)

onMounted(() => {
	if (editor.value) {
		editor.value.on('selectionUpdate', updateActiveStates)
	}
})

onBeforeUnmount(() => {
	if (editor.value) {
		editor.value.off('selectionUpdate', updateActiveStates)
	}
})
</script>

<style lang="scss">
.tiptap-editor {
	width: 100%;
	border: 1px solid #dcdfe6;
	border-radius: 4px;

	table {
		position: relative;
		border-collapse: collapse;
		margin: 1rem 0;

		td,
		th {
			position: relative;
			border: 1px solid #000 !important;
			padding: 8px 12px;
			min-width: 50px;

			&.selectedCell {
				background-color: #f0f0f0;
			}
		}

		.resize-handle {
			position: absolute;
			right: -2px;
			top: 0;
			bottom: 0;
			width: 5px;
			background-color: #adadad;
			cursor: col-resize;
			z-index: 20;
			opacity: 0;
			transition: opacity 0.2s;

			&:hover {
				opacity: 1;
			}
		}

		&:hover .resize-handle {
			opacity: 0.5;
		}
	}

	.tableWrapper {
		overflow-x: auto;
		margin: 1rem 0;
	}

	blockquote {
		border-left: 3px solid #ddd;
		margin-left: 0;
		padding-left: 1em;
		color: #666;
	}

	pre {
		background: #f5f7fa;
		padding: 1em;
		border-radius: 4px;
		code {
			color: #333;
		}
	}

	hr {
		border: none;
		border-top: 2px solid #ddd;
		margin: 1em 0;
	}

	ul,
	ol {
		padding-left: 1.5em;
	}

	a {
		color: #409eff;
		text-decoration: underline;
	}

	.toolbar {
		padding: 5px;
		border-bottom: 1px solid #dcdfe6;
		background: #f5f7fa;
		position: sticky;
		top: 50px;
		z-index: 100;
	}

	.editor-content {
		min-height: 500px;
		padding-top: 60px;

		&:focus {
			outline: none;
		}

		img {
			max-width: 100%;
			height: auto;
		}

		video {
			max-width: 100%;
			height: auto;
		}
	}
}

.resizable-image {
	position: relative;
	display: inline-block;
	max-width: 100%;
}

.resizable-image .resize-handle {
	position: absolute;
	right: -8px;
	bottom: -8px;
	width: 16px;
	height: 16px;
	background: #409eff;
	border-radius: 50%;
	cursor: se-resize;
	opacity: 0;
	transition: opacity 0.2s;
}

.resizable-image:hover .resize-handle {
	opacity: 1;
}

.resizable-image-wrapper {
	position: relative;
	display: inline-block;
	line-height: 0;
}

.resize-handle {
	position: absolute;
	right: -8px;
	bottom: -8px;
	width: 16px;
	height: 16px;
	background: #409eff;
	border-radius: 50%;
	cursor: se-resize;
	opacity: 0;
	transition: opacity 0.2s;
}

.resizable-image-wrapper:hover .resize-handle {
	opacity: 1;
}

.resizable-image {
	max-width: 100%;
	height: auto;
	user-select: none;
}

.el-button.is-active {
	background-color: var(--el-color-primary-light-9);
	border-color: var(--el-color-primary);
}

.tiptap-editor {
	p[style*='line-height'],
	h1[style*='line-height'],
	h2[style*='line-height'],
	h3[style*='line-height'] {
		margin: 0.5em 0;
		line-height: inherit !important;
	}
}
</style>
